幾乎所有的程式語言都能設變數並且儲存值,之後我們可以從變數取值或是修改變數的值,這種利用變數儲存值,可供我們使用的機制,讓程式語言的執行過程中,保留了某種狀態。
當使用了變數之後,問題是要去哪找這變數呢?
所以就必須定義一組規範,讓我們在需要的時候,可以找到變數,而所謂的規範可稱之為:範疇
一般認為JavaScript是「dynamic(動態)」或「interpreted(直譯)」式的程式語言,但本書作者認為JavaScript是屬於「compiled(編譯)」式。
原文:
It may be self-evident, or it may be surprising, depending on your level of interaction with various languages, but despite the fact that JavaScript falls under the general category of "dynamic" or "interpreted" languages, it is in fact a compiled language.
傳統的編譯器處理程式原始碼(source code)會經歷3個步驟:
var a=2;
會被拆解成數個tokens:var
,a
,=
,2
,;
var
,帶有兩個子結點:
a
。2
。a
,並賦值2
。JavaScript程式碼在被執行之前,會先以非常短的時間,微秒(microseconds )或更少,進行編譯(compiled)。
JavaScript會使用JITs,lazy compile或是hot re-compile等各種方式,來達到提高效能。
JavaScript會在原始碼(source code)即將被執行之前,完成編譯,並馬上執行。
在理解範疇的過程中,有3個主要角色:
var a = 2;
這段程式碼,一般會以敘述句(statement)來看待,但實際的狀況是,Engine會將之拆成2個部分,分別由Engine以及Compiler處理。
首先,Compiler先進行語意分析,將之拆解為tokens,之後,再轉換為"AST" (Abstract Syntax Tree).
接下來的步驟,我們可能會認為,在記憶體配置一個空間給變數a
,再賦值2
,但實際上並不是如此。
Compiler會如此處理:
var a
,Compiler會跟Scope做確認,看看a
是否存在於特定的範疇集合中。若存在,Compiler會忽略該次宣告,並往下一步執行。若無,Compiler會要求Scope在範疇集合中宣告a
。a = 2
這個賦值運算式。Engine會詢問Scope在它的範疇集合中是否有a。若有,就取用。若無,則到他處尋找。a
,就會賦值2
,若最終都沒找到,則會報錯。當Engine執行上述Compiler在第二步驟(Parsing)所產生的程式碼,它會向Scope詢問,a
是否已經被宣告了。
不過,Engine所執行的查詢種類,會影響到查詢的結果。
在這個案例中,Engine會執行一種名叫LHS的查詢,另一種查詢則是叫RHS。
LHS:Left-hand Side
RHS:Right-hand Side
非明確的判斷LHS或RHS是以指定運算子(assignment operator)「=」為依據:
若變數在「=」的左邊,是LHS,右邊,則是RHS
但以上述的準則來判斷,容易產生誤解,為何如此一說?
以LHS來說,首先會找到位於左邊的變數,再進行賦值的動作。
但以RHS來說,並不表示變數非得在的右邊不可,真正的涵義應該是,變數不在「=」的右邊。
換個角度,我們可以解讀為,取出該變數的值。
console.log(a);
這邊並沒有賦值給a,而是要取出a的值,所以這個值會被傳入console.log( )
之中
那LHS呢?
a = 2;
就像剛剛說的,首先會先找到a,在這個運算式中,a是什麼值並不重要,我們的主要目的,是要把2這個值,賦值給a。
不管是LHS或RHS,都不要聚焦在字面上的意義(Left/Right-hand Side),應該要理解的是
誰是目標「who's the target of the assignment(LHS)」。
誰是來源「who's the source of the assignment(RHS)」。
我們來看看這個範例:
function foo(a) {
console.log(a); // 2
}
foo(2);
呼叫foo(2);
,意思就是,對foo
執行RHS查詢,稍早已經定義foo
為一個函式,所以我們會找到foo
的值,並執行該函式。
這邊有個細節要注意,我們呼叫foo
的同時,也傳入一個值給它,在這種情況之下,2
會做為引數指定給參數a
,所以會隱含地執行a=2;
這個指定運算式,也就是LHS。
執行到console.log(a);
這段,首先會執行對console物件的RHS查詢,Engine會查詢是否有console這個物件,並找出log這個方法。
所以我們再整理一下整個過程:
把2
傳入foo
,再由console.log
輸出,先對a
執行LHS,再對console物件執行RHS,最後,要輸出a
的值,會對a
執行RHS。
關於宣告foo
函式,如果使用var foo=function(a){…}
的話,或許會認為這是執行LHS,就跟之前對a
執行LHS一樣。
但實際上,Compiler會在code-generation這個階段,就會處理這種方式的賦值,並不會讓Engine去處理到這部分,所以如果將函示宣告視為LHS,是不恰當的。
對於上述的範例,我們做個整理:
function foo(a) {
console.log(a); // 2
}
foo(2);
foo(2);
,它會對foo
執行RHS查詢,並往scope尋找。foo
函式,所以能夠在scope中找到。2
當作引數傳給foo
函式,首先它需要找到參數a
,一樣往scope尋找。foo
函式的同時,一併也宣告參數a
,所以能夠在scope中找到。a
並賦值2
,執行LHS。a
的值給log方法,依舊往scope尋找。進階的範例,讓我們更進一步了解LHS與RHS:
function foo(a) {
var b = a;
return a + b;
}
var c = foo(2);
執行LHS查詢有:
var c = foo(2);
將foo函式賦值給c。foo(2);
隱含地執行a = 2
。var b = a;
將a的值賦值給b。執行RHS查詢有:
foo(2);
往scope查詢foo
的值。var b = a;
賦值前,先找出a
的值。return a + b;
找出a
跟b
的值,並回傳。所謂範疇,簡言之,就是一個可以讓我們藉由識別字名稱來找到變數的規範,但實際情況,我們所要尋找的範疇可能不只一個,範疇內部也可以包含另一個範疇,這種概念就是所謂的Nested Scope巢狀範疇。
如果Engine在目前的範疇找不到目標變數,它就會往外面一層的範疇尋找,直到找到,或是達到最外層的範疇(全域範疇)為止。
function foo(a) {
console.log(a + b);
}
var b = 2;
foo(2); // 4
執行b
的RHS查詢,無法在foo
內完成,所以Engine會往foo
的外部scope找。
在這個範例,外部scope是指全域範疇,並且在外部scope找到b
。
在變數未宣告的情況下,使用LHS與RHS會產生不同的結果。
function foo(a) {
console.log(a + b);
b = a;
}
foo(2);
會發生以下錯誤:
這是因為對b
使用RHS查詢,但是並沒有在所有的範疇中找到b
,因為b
並沒有被宣告。
如果RHS無法在範疇找到b
,會丟出一個ReferenceError
類型的錯誤。
但如果使用LHS查詢,一直到最外圍的範疇都沒找到目標變數的話,若不是在「嚴格模式」中,
那會在全域範疇中建立跟尋找目標同名的全域變數。
以下情況會發生TypeError
錯誤:
let a = 10;
a();
let a;
a.prop;
ReferenceError
錯誤的產生,與範疇解析的錯誤有關,表示找不到目標變數。TypeError
表示解析成功,但試圖對結果執行非法的行為。
var a=2;
為例:
var a
,在目前的範疇中,宣告a
a=2
,執行LHS查詢,在範疇中找到a
之後,賦值。ReferenceError
類型的錯誤。參考來源:
WIKI 抽象語法樹
此為You Don't Know JS系列的筆記。